﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Web.Security;
using TaskManager.Domain.Repositories;
using User = TaskManager.Domain.BusinessObjects.User;

namespace TaskManager.DataAccess.Repositories
{
    /// <summary>
    /// Users repository
    /// </summary>
    internal class UserRepository : IUserRepository
    {
        private static readonly log4net.ILog Log = log4net.LogManager.GetLogger(typeof(UserRepository));
        
        private readonly Repository<User, int> _repository;

        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="context">Database context</param>
        public UserRepository(TaskManagerDbContext context)
        {
            _repository = new Repository<User, int>(context, context.Users);

            if (Log.IsDebugEnabled) Log.Debug("Created UserRepository");
        }

        /// <summary>
        /// Searches for the user by name
        /// </summary>
        /// <param name="name">User name</param>
        /// <returns>Found user or null</returns>
        public User FindByName(string name)
        {
            return _repository.All.Where(user => user.Name == name).SingleOrDefault();
        }

        /// <summary>
        /// Searches for the user by identifier
        /// </summary>
        /// <param name="id">User identifier</param>
        /// <returns>Found user or null if user was not found</returns>
        public User Find(int id)
        {
            return _repository.Find(id);
        }

        /// <summary>
        /// Returns all available users
        /// </summary>
        public IQueryable<User> All
        {
            get { return _repository.All; }
        }

        /// <summary>
        /// Deletes user
        /// </summary>
        /// <param name="id">User identifier</param>
        /// <param name="commit">Lambda that will be invoked if user was successfully deleted in order to commit changes</param>
        /// <remarks>Since users are stored both in the application database and ASP.Net membership's database it is 
        /// recommended to commit changes in <c>commit</c> delegate</remarks>
        /// <example>
        /// userRepository.Delete(1, () => _unitOfWork.Save());
        /// </example>
        public void Delete(int id, Action commit)
        {
            User user = Find(id);

            if(user == null)
                throw new KeyNotFoundException("user not found, id: " + id);

            MembershipUserCollection userCollection = Membership.FindUsersByName(user.Name);

            if (userCollection.Count == 0)
            {
                Log.Error("User not found in membership database,user name=[" + user.Name + "],id=[" + id + "]");
            }
            else if (userCollection.Count > 1)
            {
                throw new InvalidOperationException("delete user, id: " + id);
            }

            _repository.Delete(id);
            bool res = Membership.DeleteUser(user.Name);

            if (res)
                commit();
        }

        /// <summary>
        /// Deletes user
        /// </summary>
        /// <param name="userName">User name</param>
        /// <param name="commit">Lambda that will be invoked if user was successfully deleted in order to commit changes</param>
        /// <remarks>Since users are stored both in the application database and ASP.Net membership's database it is 
        /// recommended to commit changes in <c>commit</c> delegate</remarks>
        /// <example>
        /// userRepository.Delete("user1", () => _unitOfWork.Save());
        /// </example>
        public void Delete(string userName, Action commit)
        {
            User user = FindByName(userName);

            if(user == null)
                throw new KeyNotFoundException("user not found: " + userName);

            Delete(user.Id, commit);
        }

        /// <summary>
        /// Verifies that the supplied user name and password are valid
        /// </summary>
        /// <param name="userName">User name</param>
        /// <param name="password">Password</param>
        /// <returns>true if the supplied user name and password are valid; otherwise, false.</returns>
        public bool ValidateUser(string userName, string password)
        {
            return Membership.ValidateUser(userName, password);
        }

        /// <summary>
        /// Creates a new user
        /// </summary>
        /// <param name="userName">User name</param>
        /// <param name="password">Password</param>
        /// <param name="email">Email</param>
        /// <param name="commit">Lambda that will be invoked if user was successfully created</param>
        /// <returns>Pair where the first element is <see cref="User"/> object, the second is error description</returns>
        /// <remarks>Since users are stored both in the application database and ASP.Net membership's database it is 
        /// recommended to commit changes in <c>commit</c> delegate</remarks>
        /// <example>
        /// userRepository.CreateUser("userName1", "password1", "email1", () => _unitOfWork.Save());
        /// </example>
        public Tuple<User, string> CreateUser(string userName, string password, string email, Action commit)
        {
            MembershipCreateStatus createStatus;
            Membership.CreateUser(userName, password, email, null, null, true, null, out createStatus);

            if (createStatus == MembershipCreateStatus.Success)
            {
                User user;

                try
                {
                    user = new User(userName);
                    _repository.Insert(user);
                    commit();
                }
                catch (Exception)
                {
                    Membership.DeleteUser(userName);
                    throw;
                }

                return new Tuple<User, string>(user, string.Empty);
            }

            return new Tuple<User, string>(null, ErrorCodeToString(createStatus));
        }

        /// <summary>
        /// Changes password for the given user
        /// </summary>
        /// <param name="userName">User name</param>
        /// <param name="oldPassword">Old password</param>
        /// <param name="newPassword">New password</param>
        /// <returns>true if password was successfully changes; false otherwise</returns>
        public bool ChangePassword(string userName, string oldPassword, string newPassword)
        {
            // ChangePassword will throw an exception rather
            // than return false in certain failure scenarios.
            bool changePasswordSucceeded;
            try
            {
                MembershipUser currentUser = Membership.GetUser(userName, userIsOnline: true);
                changePasswordSucceeded = currentUser.ChangePassword(oldPassword, newPassword);
            }
            catch (Exception)
            {
                changePasswordSucceeded = false;
            }

            return changePasswordSucceeded;
        }

        private static string ErrorCodeToString(MembershipCreateStatus createStatus)
        {
            // See http://go.microsoft.com/fwlink/?LinkID=177550 for
            // a full list of status codes.
            switch (createStatus)
            {
                case MembershipCreateStatus.DuplicateUserName:
                    return Resource.UserRepository_ErrorCodeToString_User_name_already_exists;

                case MembershipCreateStatus.DuplicateEmail:
                    return Resource.UserRepository_ErrorCodeToString_A_user_name_for_that_e_mail_address_already_exists;

                case MembershipCreateStatus.InvalidPassword:
                    return Resource.UserRepository_ErrorCodeToString_The_password_provided_is_invalid;

                case MembershipCreateStatus.InvalidEmail:
                    return Resource.UserRepository_ErrorCodeToString_The_e_mail_address_provided_is_invalid;

                case MembershipCreateStatus.InvalidAnswer:
                    return Resource.UserRepository_ErrorCodeToString_The_password_retrieval_answer_provided_is_invalid;

                case MembershipCreateStatus.InvalidQuestion:
                    return Resource.UserRepository_ErrorCodeToString_The_password_retrieval_question_provided_is_invalid;

                case MembershipCreateStatus.InvalidUserName:
                    return Resource.UserRepository_ErrorCodeToString_The_user_name_provided_is_invalid;

                case MembershipCreateStatus.ProviderError:
                    return Resource.UserRepository_ErrorCodeToString_The_authentication_provider_returned_an_error;

                case MembershipCreateStatus.UserRejected:
                    return Resource.UserRepository_ErrorCodeToString_The_user_creation_request_has_been_canceled;

                default:
                    return Resource.UserRepository_ErrorCodeToString_An_unknown_error_occurred;
            }
        }
    }
}